package com.qibao.toolkit.exception.config;

import com.qibao.toolkit.exception.handler.GlobalErrorController;
import com.qibao.toolkit.exception.handler.GlobalExceptionHandler;
import com.qibao.toolkit.exception.strategy.factory.impl.BaseSpringExceptionHandleStrategyHolder;
import javassist.*;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ClassFile;
import javassist.bytecode.ConstPool;
import javassist.bytecode.annotation.Annotation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo.BuilderConfiguration;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.stream.Stream;

/**
 * @author wangcheng
 * @version $Id: ExceptionHandleConfiguration.java, v0.1 2019/4/26 14:54 wangcheng Exp $$
 */
public class ExceptionHandleConfiguration implements ImportBeanDefinitionRegistrar {

    private static Logger logger = LoggerFactory.getLogger(ExceptionHandleConfiguration.class);

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        registryExceptionHandleStrategyHolder(registry);
        registryGlobalErrorController(registry);
        registryGlobalExceptionHandler(registry);
    }

    private void registryExceptionHandleStrategyHolder(BeanDefinitionRegistry registry) {
        BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(BaseSpringExceptionHandleStrategyHolder.class);
        AbstractBeanDefinition beanDefinition = bdb.getBeanDefinition();
        registry.registerBeanDefinition(BaseSpringExceptionHandleStrategyHolder.EXCEPTION_HANDLE_STRATEGY_HOLDER_BEAN_NAME,beanDefinition);
    }

    private void registryGlobalExceptionHandler(BeanDefinitionRegistry registry) {
        try {
            Class handlerClazz = generateGlobalExceptionClass();
            BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(handlerClazz);
            AbstractBeanDefinition beanDefinition = bdb.getBeanDefinition();
            registry.registerBeanDefinition(GlobalExceptionHandler.DEFAULT_GLOBAL_EXCEPTION_HANDLE_BEAN_NAME,beanDefinition);
        } catch (Exception e) {
            logger.error("注册全局异常处理器失败",e);
            throw new IllegalStateException("注册全局异常处理器失败");
        }
    }

    private void registryGlobalErrorController(BeanDefinitionRegistry registry){
        try {
            BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(generateGlobalErrorClass());
            AbstractBeanDefinition beanDefinition = bdb.getBeanDefinition();
            registry.registerBeanDefinition(GlobalErrorController.DEFAULT_GLOBAL_ERROR_HANDLE_BEAN_NAME,beanDefinition);
        } catch (Exception e) {
            logger.error("注册全局错误处理器失败",e);
            throw new IllegalStateException("注册全局错误处理器失败");
        }
    }


    private Class generateGlobalExceptionClass() throws CannotCompileException, NotFoundException {
        return baseGenerateProxyClass(GlobalExceptionHandler.class, RestControllerAdvice.class.getName());
    }

    private Class generateGlobalErrorClass() throws CannotCompileException, NotFoundException {
        return baseGenerateProxyClass(GlobalErrorController.class, RestController.class.getName());
    }

    private Class baseGenerateProxyClass(Class targetClass,String annotationClassName)
            throws NotFoundException, CannotCompileException {
        ClassPool pool = ClassPool.getDefault();
        ClassClassPath ccpath = new ClassClassPath(targetClass);
        pool.insertClassPath(ccpath);
        CtClass clazz = pool.get(targetClass.getName());
        ClassFile ccFile = clazz.getClassFile();
        ConstPool constpool = ccFile.getConstPool();
        AnnotationsAttribute classAttr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
        Annotation annotation = new Annotation(annotationClassName,constpool);
        classAttr.addAnnotation(annotation);
        ccFile.addAttribute(classAttr);
        clazz.setName(targetClass.getName() + "$proxy");
        return clazz.toClass();
    }

    /**
     *
     * @param environment
     * @param handlerMapping
     * @param handler
     */
    public void initErrorControllerMapping(Environment environment, RequestMappingHandlerMapping handlerMapping, Object handler){
        final Class<?> userType = ClassUtils.getUserClass(handler.getClass());
        Map<Method, RequestMappingInfo> methods = MethodIntrospector.selectMethods(userType,
                (MethodIntrospector.MetadataLookup<RequestMappingInfo>) method -> {
                    try {
                        return getMappingForMethod(handlerMapping,environment,method, userType);
                    }
                    catch (Throwable ex) {
                        throw new IllegalStateException("Invalid mapping on handler class [" +
                                userType.getName() + "]: " + method, ex);
                    }
                });
        for (Map.Entry<Method, RequestMappingInfo> entry : methods.entrySet()) {
            Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType);
            RequestMappingInfo mapping = entry.getValue();
            try {
                //判断是否已经存在 要注入的mapping，存在的话 提示 “当前系统已经存在 请禁用全局异常处理，或者禁用已经的mapping”
                handlerMapping.registerMapping(mapping,handler,invocableMethod);
            } catch (IllegalStateException e) {
                throw new IllegalArgumentException("当前系统已经存在requestMapping:"+mapping+" 请禁用全局异常处理，或者禁用已经存在requestMapping");
            }
        }
    }

    private RequestMappingInfo getMappingForMethod(RequestMappingHandlerMapping handlerMapping, Environment environment, Method method, Class<?> handlerType) {
        RequestMappingInfo info = createRequestMappingInfo(handlerMapping,environment,method);
        if (info != null) {
            RequestMappingInfo typeInfo = createRequestMappingInfo(handlerMapping,environment,handlerType);
            if (typeInfo != null) {
                info = typeInfo.combine(info);
            }
        }
        return info;
    }
    private RequestMappingInfo createRequestMappingInfo(RequestMappingHandlerMapping handlerMapping, Environment environment, AnnotatedElement element) {
        RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
        return (requestMapping != null ? createRequestMappingInfo(handlerMapping,environment,requestMapping) : null);
    }
    private RequestMappingInfo createRequestMappingInfo(RequestMappingHandlerMapping handlerMapping, Environment environment,
                                                        RequestMapping requestMapping) {
        String[] path = requestMapping.path();

        String[] resolvePlacePath = Stream.of(path).map(environment::resolvePlaceholders).toArray(String[]::new);

        RequestMappingInfo.Builder builder = RequestMappingInfo
                .paths(resolvePlacePath)
                .methods(requestMapping.method())
                .params(requestMapping.params())
                .headers(requestMapping.headers())
                .consumes(requestMapping.consumes())
                .produces(requestMapping.produces())
                .mappingName(requestMapping.name());
        return builder.options(builderConfiguration(handlerMapping)).build();
    }

    private BuilderConfiguration builderConfiguration(RequestMappingHandlerMapping handlerMapping) {
        RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
        config.setUrlPathHelper(handlerMapping.getUrlPathHelper());
        config.setPathMatcher(handlerMapping.getPathMatcher());
        config.setContentNegotiationManager(handlerMapping.getContentNegotiationManager());
        return config;
    }

}
